/**
 * GarinDriver -- Eclipse Public License - v 1.0
 * http://www.eclipse.org/legal/epl-v10.html
 */
package garin.parse;

import garin.parse.io.AbstractSource;
import garin.parse.io.Source;

import java.io.IOException;
import java.util.ArrayList;

/**
 * Token from a source
 * <p>
 * A token is defined as an area of the source with a start and an end.
 * <p>
 * By using start/end we don't have to materialize the string until the last
 * moment and when doing so we can capture the immutable string once and not
 * grow it in a buffer.
 * 
 * @author Mark Farnsworth
 */
public abstract class Token extends AbstractSource implements Cloneable {
	/**
	 * End position for the token.
	 */
	protected long end = -1;
	/**
	 * Start position for the token.
	 */
	protected long start = -1;
	private final Token parent;
	private ArrayList<Token> expected = new ArrayList<Token>();
	private String value;
	private Source source;
	
	protected Token() {
		parent = null;
	}
	
	/**
	 * @param source
	 */
	protected Token(final Token parent) {
		this.parent = parent;
	}
	
	public boolean accept() throws Exception {
		return false;
	}
	
	public final void add(final Token t) {
		expected.add(t);
	}
	
	public final long at() {
		return source().at();
	}
	
	public final char ch() {
		return source().ch();
	}
	
	public void clear() {
		expected.clear();
	}
	
	@Override
	public final Token clone() throws CloneNotSupportedException {
		final Token clone = (Token) super.clone();
		clone.expected = new ArrayList<Token>(expected);
		return clone;
	}
	
	/**
	 * @return string contents
	 * @throws IOException
	 */
	public final String contents() throws IOException {
		final long size = size();
		if (size <= 0) return null;
		final String contents = contents(start, start + size);
		return contents;
	}
	
	public final String contents(final long start, final long end)
			throws IOException {
		return source().contents(start, end);
	}
	
	public final long end() {
		return end == -1 ? at() : end;
	}
	
	@Override
	public final boolean equals(final Object obj) {
		return super.equals(obj);
	}
	
	public void exit() throws Exception {
		end = at() - 1;
		if (size() <= 0) return;
		final String contents = contents();
		value(contents);
	}
	
	@Override
	protected final void finalize() throws Throwable {
		super.finalize();
	}
	
	public final boolean go() throws IOException {
		return source().go();
	}
	
	public final boolean go(final long pos) throws IOException {
		return source().go(pos);
	}
	
	@Override
	public final int hashCode() {
		return super.hashCode();
	}
	
	public final boolean matchClass(final Class<?> cz) {
		Token t = this;
		while (t != null)
			try {
				cz.cast(t);
				return true;
			} catch (final ClassCastException e) {
				t = t.parent();
			}
		return false;
	}
	
	/**
	 * Handles the current char.
	 * 
	 * @return new mode (often returns self as the new mode)
	 * @throws IOException
	 * @throws CloneNotSupportedException
	 * @throws Exception
	 */
	public Token on() throws Exception {
		self();
		final Object[] list = expected.toArray();
		int i = 0;
		while (i < list.length) {
			final Token item = (Token) list[i++];
			final boolean accept = item.accept();
			if (accept) try {
				final long size = size();
				if (size > 1) exit();
				reset();
				final Token t = item.clone();
				return t.on();
			} catch (final CloneNotSupportedException e) {
				throw new RuntimeException(e);
			}
		}
		return self();
	}
	
	public final Token parent() {
		return parent;
	}
	
	private void reset() {
		start = -1;
		end = -1;
	}
	
	public final Token self() {
		if (-1 == start) start = at();
		return this;
	}
	
	/**
	 * @return size of the token
	 */
	public final long size() {
		return -1 == start ? -1 : end() - start + 1;
	}
	
	protected final Source source() {
		return source == null ? parent.source() : source;
	}
	
	protected final void source(final Source s) {
		source = s;
	}
	
	public final long start() {
		return start;
	}
	
	@Override
	public final String toString() {
		Token p = parent();
		String s = super.toString();
		while (p != null) {
			s += ", " + p.getClass().getName();
			p = p.parent();
		}
		return "[" + start() + "-" + end() + "] " + s;
	}
	
	/**
	 * @return mode one level up (or null)
	 * @throws Exception
	 */
	public final Token up() throws Exception {
		exit();
		final Token t = parent();
		if (t == null) return null;
		return t.on();
	}
	
	public final String value() {
		try {
			return value == null ? contents() : value;
		} catch (final IOException e) {
			return null;
		}
	}
	
	public final void value(final String value) throws Exception {
		this.value = value;
		source().setValueObserver().onSetValue(this);
	}
}
